#include "map.h"
#include "game.h"
#include "actor.h"
#include "symbol.h"
#include "bspecial.h"
#include "creature.h"
#include "item.h"
#include "random.h"
#include "player.h"

Tile::Tile()
: _type( WALL )
, _memoryActor( 0 )
{
}

Tile::~Tile()
{
}

uint Tile::GetType() const
{
    return _type;
}

void Tile::SetType( uint type )
{
    _type = type;
}

std::string Tile::GetName( const Map& map ) const
{
    if ( _type == FLOOR )
        return "the ground";
    if ( _type == WALL ) {
        if ( map.IsOutsideMap() )
            return "a tree";
        return "a wall";
    }
    if ( _type == ROCK ) {
        if ( map.IsOutsideMap() )
            return "a rock";
        return "a rock wall";
    }
    if ( _type == WATER )
        return "water";
    if ( _type == LAVA )
        return "lava";
    return "unknown tile";
}

void Tile::Memorise( const Map& map, uint x, uint y, bool itemsInDark )
{
    _tileSymbol.clear();

    // Variation hash: 0 to 100
    uint hash = 17 * ( x + x * x + x * x * x ) * 19 * ( y + y * y + y * y * y ) +
                23 * map.GetWidth() + 29 * map.GetHeight();
    hash %= 101;
    uint mapType = map.GetType();

    // Identical adjacents
    std::vector< bool > ia;
    for ( int j = -1; j <= 1; j++ ) {
        for ( int i = -1; i <= 1; i++ ) {
            int ix = int( x ) + i;
            int jy = int( y ) + j;
            if ( ix < 0 || ix >= int( map.GetWidth() ) || jy < 0 || jy >= int( map.GetHeight() ) )
                ia.push_back( true );
            else
                ia.push_back( map.GetTile( ix, jy ).GetType() == _type ||
                              ( GetType() == WALL && map.GetTile( ix, jy ).GetType() == ROCK ) );
        }
    }
    bool up    = false;
    bool down  = false;
    bool left  = false;
    bool right = false;

    if ( ia[ 1 ] && ( !( ia[ 0 ] && ia[ 2 ] ) || !( ia[ 3 ] && ia[ 5 ] ) ) )
        up = true;
    if ( ia[ 7 ] && ( !( ia[ 6 ] && ia[ 8 ] ) || !( ia[ 3 ] && ia[ 5 ] ) ) )
        down = true;
    if ( ia[ 3 ] && ( !( ia[ 0 ] && ia[ 6 ] ) || !( ia[ 1 ] && ia[ 7 ] ) ) )
        left = true;
    if ( ia[ 5 ] && ( !( ia[ 2 ] && ia[ 8 ] ) || !( ia[ 1 ] && ia[ 7 ] ) ) )
        right = true;

    if ( _type == FLOOR ) {
        _tileSymbol.push_back( IO::Sym( Sym::FULLSTOP, Colour( 192, 192, 192 ) ) );
        if ( ( mapType == Map::DUNGEON && hash <  2 ) ||
             ( mapType == Map::CAVERN  && hash < 35 ) ||
             ( mapType == Map::BARRENS && hash < 50 ) ||
             ( mapType == Map::PLANE   && hash <  1 ) )
             _tileSymbol[ 0 ].first = Sym::COMMA;
        if ( mapType == Map::BARRENS ) {
            if ( hash >= 42 && hash < 52 )
                _tileSymbol[ 0 ].second = Colour( 64, 192, 64 );
            else
                _tileSymbol[ 0 ].second = Colour( 128, 128, 64 );
        }
        if ( mapType == Map::PLANE )
            _tileSymbol[ 0 ].second = Colour( 128, 64, 128 );
    }
    else if ( _type == WATER ) {
        _tileSymbol.push_back( IO::Sym( Sym::APPROX, Colour( 64, 64, 192 ) ) );
        _tileSymbol.push_back( IO::Sym( '~', Colour( 64, 64, 192 ) ) );
    }
    else if ( _type == LAVA ) {
        _tileSymbol.push_back( IO::Sym( Sym::APPROX, Colour( 192, 64, 64 ) ) );
        _tileSymbol.push_back( IO::Sym( '~', Colour( 192, 64, 64 ) ) );
    }
    else if ( _type == WALL ) {
        uchar s = ' ';
        if ( mapType == Map::DUNGEON ) {
            if      (  up &&  down &&  left &&  right )
                s = Sym::WALLDVERTDHORZ;
            else if ( !up &&  down &&  left &&  right )
                s = Sym::WALLDDOWNDHORZ;
            else if (  up && !down &&  left &&  right )
                s = Sym::WALLDUPDHORZ;
            else if (  up &&  down && !left &&  right )
                s = Sym::WALLDVERTDRIGHT;
            else if (  up &&  down &&  left && !right )
                s = Sym::WALLDVERTDLEFT;
            else if ( !up &&  down && !left &&  right )
                s = Sym::WALLDDOWNDRIGHT;
            else if ( !up &&  down &&  left && !right )
                s = Sym::WALLDDOWNDLEFT;
            else if (  up && !down && !left &&  right )
                s = Sym::WALLDUPDRIGHT;
            else if (  up && !down &&  left && !right )
                s = Sym::WALLDUPDLEFT;
            else if (  up ||  down                    )
                s = Sym::WALLDVERT;
            else if (                  left ||  right )
                s = Sym::WALLDHORZ;
            else
                s = Sym::BOX;
        }
        else if ( mapType == Map::CAVERN || mapType == Map::PLANE ) {
            if      (  up &&  down &&  left &&  right )
                s = Sym::WALLSVERTSHORZ;
            else if ( !up &&  down &&  left &&  right )
                s = Sym::WALLSDOWNSHORZ;
            else if (  up && !down &&  left &&  right )
                s = Sym::WALLSUPSHORZ;
            else if (  up &&  down && !left &&  right )
                s = Sym::WALLSVERTSRIGHT;
            else if (  up &&  down &&  left && !right )
                s = Sym::WALLSVERTSLEFT;
            else if ( !up &&  down && !left &&  right )
                s = Sym::WALLSDOWNSRIGHT;
            else if ( !up &&  down &&  left && !right )
                s = Sym::WALLSDOWNSLEFT;
            else if (  up && !down && !left &&  right )
                s = Sym::WALLSUPSRIGHT;
            else if (  up && !down &&  left && !right )
                s = Sym::WALLSUPSLEFT;
            else if (  up ||  down                    )
                s = Sym::WALLSVERT;
            else if (                  left ||  right )
                s = Sym::WALLSHORZ;
            else
                s = Sym::HASH;
        }
        else
            s = Sym::HASH;

        if ( ( hash < 4 && mapType == Map::DUNGEON ) ||
             ( hash < 8 && mapType == Map::PLANE ) )
            s = Sym::BOX;
        if ( ( hash < 60 && mapType == Map::CAVERN ) ||
             ( hash >= 94 && mapType == Map::PLANE ) )
            s = Sym::HASH;

        Colour c( 192, 192, 192 );
        if ( mapType == Map::BARRENS ) {
            if ( hash >= 35 && hash < 55 )
                c = Colour( 64, 192, 64 );
            else
                c = Colour( 128, 128, 64 );
        }
        if ( mapType == Map::PLANE )
            c = Colour( 128, 64, 128 );
        _tileSymbol.push_back( IO::Sym( s, c ) );
    }
    else if ( _type == ROCK ) {
        _tileSymbol.push_back( IO::Sym( Sym::HASH, Colour( 192, 192, 192 ) ) );
        if ( mapType == Map::PLANE )
            _tileSymbol[ 0 ].second = Colour( 128, 64, 128 );
    }

    ActorList list;
    GetActorsOnTile( list );
    foreach ( Actor* a, list ) {
        bool memory, tileMemory;
        if ( a->Call< BIMemorySymbol >( &memory, &tileMemory ) && memory && tileMemory ) {
            _tileSymbol.clear();
            a->Call< BISymbol >( &_tileSymbol );
            break;
        }
    }

    Item* ignore;
    if ( map.GetGame().GetPlayer()->CanSeeTile( x, y ) || ( itemsInDark && ( !_memoryActor.IsValid() || _memoryActor->Call< BIIsItem >( &ignore ) ) ) ) {
        _actorSymbol.clear();
        foreach ( Actor* a, list ) {
            bool memory, tileMemory;
            if ( a->Call< BIMemorySymbol >( &memory, &tileMemory ) && memory && ( a->CanBeSeenByPlayer() || ( itemsInDark && a->Call< BIIsItem >( &ignore ) ) ) && !tileMemory ) {
                a->Call< BISymbol >( &_actorSymbol );
                _memoryActor = a;
                break;
            }
        }
    }
}

const IO::SymList& Tile::GetSymbol() const
{
    return _actorSymbol.empty() ? _tileSymbol : _actorSymbol;
}

void Tile::Add( Actor* actor )
{
    if ( actor == 0 )
        throw std::runtime_error( "Null actor added to tile" );

    if ( IsActorOnTile( actor ) )
        throw std::runtime_error( "Duplicate actor added to tile" );
    _actors.insert( actor );
}

void Tile::Remove( Actor* actor )
{
    if ( actor == 0 )
        throw std::runtime_error( "Null actor removed from tile" );
    if ( !IsActorOnTile( actor ) )
        throw std::runtime_error( "Duplicate actor added to tile" );
    _actors.erase( actor );
}

bool Tile::IsEmpty() const
{
    return _actors.empty();
}

bool Tile::IsActorOnTile( const Actor* actor ) const
{
    return _actors.count( const_cast< Actor* >( actor ) ) != 0;
}

void Tile::GetActorsOnTile( ActorList& outputList )
{
    foreach ( Actor* a, _actors )
        outputList.push_back( a );
    std::sort( outputList.begin(), outputList.end(), &Actor::PrioritySort );
}

void Tile::GetActorsOnTile( ConstActorList& outputList ) const
{
    foreach ( const Actor* a, _actors )
        outputList.push_back( a );
    std::sort( outputList.begin(), outputList.end(), &Actor::PrioritySort );
}

bool Tile::IsBlocked() const
{
    return IsBlockedByTerrain() || IsBlockedByActor();
}

bool Tile::IsBlockedByTerrain() const
{
    return _type == WALL || _type == ROCK;
}

bool Tile::IsBlockedByActor() const
{
    foreach ( const Actor* a, _actors ) {
        bool b = false;
        bool ignore;
        uint uignore;
        a->Call< BIBlocking >( &b, &ignore, &uignore );
        if ( b )
            return true;
    }

    return false;
}

bool Tile::IsLOSBlocked() const
{
    if ( _type == WALL || _type == ROCK )
        return true;

    foreach ( const Actor* a, _actors ) {
        bool b;
        bool ignore;
        bool invis;
        uint uignore;
        a->Call< BIBlocking >( &ignore, &b, &uignore );
        a->Call< BIInvisible >( &invis );
        if ( b && !invis )
            return true;
    }

    return false;
}

Map::Map( Game& game, uint width, uint height )
: _game( game )
, _width( width )
, _height( height )
, _type( DUNGEON )
{
    for ( uint i = 0; i < _width * _height; i++ )
        _tiles.push_back( Tile() );
    for ( uint i = 0; i < _width * _height; i++ )
        _lightValues.push_back( 0.0 );
}

Map::~Map()
{
}

void Map::DeleteAll()
{
    ActorList t;
    GetActorsOnMap( t );
    foreach ( Actor* a, t )
        a->Destroy();
    _game.DeleteItems();
    _game.DeleteBuffs();
    _game.DeleteActors();
}

SyncIO Map::GetSyncIO() const
{
    return _game.GetSyncIO();
}

IO& Map::GetAsyncIO() const
{
    return _game.GetAsyncIO();
}

Game& Map::GetGame()
{
    return _game;
}

const Game& Map::GetGame() const
{
    return _game;
}

Tile& Map::GetTile( uint x, uint y )
{
    if ( x < 0 || x >= _width || y < 0 || y >= _height )
        throw std::runtime_error( "Getting out of bounds tile" );

    return _tiles[ y * _width + x ];
}

const Tile& Map::GetTile( uint x, uint y ) const
{
    if ( x < 0 || x >= _width || y < 0 || y >= _height )
        throw std::runtime_error( "Getting out of bounds tile" );

    return _tiles[ y * _width + x ];
}

uint Map::GetWidth() const
{
    return _width;
}

uint Map::GetHeight() const
{
    return _height;
}

uint Map::GetType() const
{
    return _type;
}

void Map::SetType( uint type )
{
    _type = type;
}

bool Map::IsOutsideMap() const
{
    return _type == BARRENS;
}

bool Map::IsWithinMap( int x, int y ) const
{
    return x >= 0 && y >= 0 && x < int( GetWidth() ) && y < int( GetHeight() );
}

void Map::GetFreeTile( int& x, int& y, bool blockable ) {
    int n = 0;
    for ( uint xx = 0; xx < _width; xx++ )
        for ( uint yy = 0; yy < _height; yy++ )
            if ( GetTile( xx, yy ).GetType() == Tile::FLOOR && GetTile( xx, yy ).IsEmpty() &&
                 ( blockable || !IsTileBlockable( xx, yy ) ) )
                n++;
    int r = Random::Int( n );
    n = 0;
    for ( uint xx = 0; xx < _width; xx++ ) {
        for ( uint yy = 0; yy < _height; yy++ ) {
            if ( GetTile( xx, yy ).GetType() == Tile::FLOOR && GetTile( xx, yy ).IsEmpty() &&
                 ( blockable || !IsTileBlockable( xx, yy ) ) ) {
                n++;
                if ( r < n ) {
                    x = xx;
                    y = yy;
                    return;
                }
            }
        }
    }
    x = 0;
    y = 0;
}

bool Map::IsTileBlockable( int x, int y ) const
{
    int adj[ 3 ][ 3 ];
    int walls = 0;
    for ( int a = 0; a < 3; a++ ) {
        for ( int b = 0; b < 3; b++ ) {
            if ( x + a - 1 < 0 || x + a - 1 >= int( _width ) || y + b - 1 < 0 || y + b - 1 >= int( _height ) ||
                 GetTile( x + a - 1, y + b - 1 ).GetType() != Tile::FLOOR ) {
                adj[ a ][ b ] = 1;
                walls++;
            }
            else {
                if ( GetTile( x + a - 1, y + b - 1 ).IsBlocked() )
                    adj[ a ][ b ] = 3;
                else
                    adj[ a ][ b ] = 0;
            }
        }
    }

    int groups = 0;
    bool search = true;
    while ( search ) {
        bool go = false;
        while ( true ) {
            bool found = false;
            for ( int a = 0; a < 3; a++ ) {
                for ( int b = 0; b < 3; b++ ) {
                    if ( adj[ a ][ b ] == 1 && !go ) {
                        adj[ a ][ b ] = 2;
                        go = true;
                        found = true;
                    }
                    if ( adj[ a ][ b ] == 1 && go ) {
                        if ( ( a > 0 && adj[ a - 1 ][ b ] == 2 ) ||
                             ( a < 2 && adj[ a + 1 ][ b ] == 2 ) ||
                             ( b > 0 && adj[ a ][ b - 1 ] == 2 ) ||
                             ( b < 2 && adj[ a ][ b + 1 ] == 2 ) ) {
                            adj[ a ][ b ] = 2;
                            found = true;
                        }
                    }
                }
            }
            if ( !found ) {
                if ( go ) {
                    groups++;
                    for ( int a = 0; a < 3; a++ )
                        for ( int b = 0; b < 3; b++ )
                            if ( adj[ a ][ b ] == 2 )
                                adj[ a ][ b ] = 0;
                    go = false;
                }
                else
                    search = false;
                break;
            }
        }
    }
    int bGroups = 0;
    search = true;
    while ( search ) {
        bool go = false;
        while ( true ) {
            bool found = false;
            for ( int a = 0; a < 3; a++ ) {
                for ( int b = 0; b < 3; b++ ) {
                    if ( adj[ a ][ b ] == 3 && !go ) {
                        adj[ a ][ b ] = 4;
                        go = true;
                        found = true;
                    }
                    if ( adj[ a ][ b ] == 3 && go ) {
                        if ( ( a > 0 && adj[ a - 1 ][ b ] == 4 ) ||
                             ( a < 2 && adj[ a + 1 ][ b ] == 4 ) ||
                             ( b > 0 && adj[ a ][ b - 1 ] == 4 ) ||
                             ( b < 2 && adj[ a ][ b + 1 ] == 4 ) ) {
                            adj[ a ][ b ] = 4;
                            found = true;
                        }
                    }
                }
            }
            if ( !found ) {
                if ( go ) {
                    bGroups++;
                    for ( int a = 0; a < 3; a++ )
                        for ( int b = 0; b < 3; b++ )
                            if ( adj[ a ][ b ] == 4 )
                                adj[ a ][ b ] = 0;
                    go = false;
                }
                else
                    search = false;
                break;
            }
        }
    }
    return !( groups + bGroups <= 1 );
}

bool Map::MapActorSorter::operator()( const Map::MapActor& less, const Map::MapActor& greater ) const
{
    return less.a < greater.a;
}

void Map::Add( Actor* actor, int time )
{
    if ( actor == 0 )
        throw std::runtime_error( "Null actor added to map" );

    if ( IsActorOnMap( actor ) )
        throw std::runtime_error( "Duplicate actor added to map" );

    MapActor t( actor, time );
    _actors.insert( t );
}

int Map::Remove( Actor* actor )
{
    if ( actor == 0 )
        throw std::runtime_error( "Null actor removed from map" );

    if ( !IsActorOnMap( actor ) )
        throw std::runtime_error( "Actor removed from unoccupied map" );

    MapActorSet::iterator i = _actors.find( MapActor( actor, 0 ) );
    if ( i != _actors.end() ) {
        int r = i->t;
        _actors.erase( i );
        return r;
    }
    return 0;
}

bool Map::IsActorOnMap( const Actor* actor ) const
{
    return _actors.count( MapActor( const_cast< Actor* >( actor ), 0 ) ) != 0;
}

void Map::GetActorsOnMap( ActorList& outputList )
{
    foreach ( const MapActor& a, _actors )
        if ( !a.a->IsDestroyed() )
            outputList.push_back( a.a );
}

void Map::GetActorsOnMap( ConstActorList& outputList ) const
{
    foreach ( const MapActor& a, _actors )
        if ( !a.a->IsDestroyed() )
            outputList.push_back( a.a );
}

bool Map::IsInLOS( int ax, int ay, int bx, int by ) const
{
    if ( ax < 0 || ax >= int( _width ) || ay < 0 || ay >= int( _height ) ||
         bx < 0 || bx >= int( _width ) || by < 0 || by >= int( _height ) )
         throw std::runtime_error( "Calculating out of bounds line of sight" );

    bool swapped = false;
    // Centre to corner line walk
    for ( int i = 0; i < 4; ++i ) {
        double dax = !swapped ? 0.0 : i < 2 ? -0.5 : 0.5;
        double day = !swapped ? 0.0 : i % 2 ? -0.5 : 0.5;
        double dbx =  swapped ? 0.0 : i < 2 ? -0.5 : 0.5;
        double dby =  swapped ? 0.0 : i % 2 ? -0.5 : 0.5;
        if ( abs( ax + dax - bx - dbx ) >= abs( ay + day - by - dby ) ) {
            bool b = true;
            if ( ax > bx ) {
                std::swap( ax, bx );
                std::swap( ay, by );
                std::swap( dax, dbx );
                std::swap( day, dby );
                swapped = !swapped;
            }
            for ( int x = ax ; x <= bx; x++ ) {
                double d = ( x - ax - dax ) * ( by + dby - ay - day ) / ( bx + dbx - ax - dax ) + ( ay + day ) + 0.5;

                int y = int( d );
                if ( ( x != ax || y != ay ) && ( x != bx || y != by ) && !( x < 0 || x >= int( _width ) || y < 0 || y >= int( _height ) ) && GetTile( x, y ).IsLOSBlocked() ) {
                    b = false;
                    break;
                }

                y = int( d - 0.5 );
                if ( double( uint( d ) ) == d && ( x != ax || y != ay ) && ( x != bx || y != by ) && !( x < 0 || x >= int( _width ) || y < 0 || y >= int( _height ) ) && GetTile( x, y ).IsLOSBlocked() ) {
                    b = false;
                    break;
                }
            }
            if ( b )
                return true;
        }

        if ( abs( ay + day - by - dby ) >= abs( ax + dax - bx - dbx ) ) {
            bool b = true;
            if ( ay > by ) {
                std::swap( ax, bx );
                std::swap( ay, by );
                std::swap( dax, dbx );
                std::swap( day, dby );
                swapped = !swapped;
            }
            for ( int y = ay; y <= by; y++ ) {
                double d = ( y - ay - day ) * ( bx + dbx - ax - dax ) / ( by + dby - ay - day ) + ( ax + dax ) + 0.5;

                int x = int( d );
                if ( ( x != ax || y != ay ) && ( x != bx || y != by ) && !( x < 0 || x >= int( _width ) || y < 0 || y >= int( _height ) ) && GetTile( x, y ).IsLOSBlocked() ) {
                    b = false;
                    break;
                }

                x = int( d - 0.5 );
                if ( double( uint( d ) ) == d && ( x != ax || y != ay ) && ( x != bx || y != by ) && !( x < 0 || x >= int( _width ) || y < 0 || y >= int( _height ) ) && GetTile( x, y ).IsLOSBlocked() ) {
                    b = false;
                    break;
                }
            }
            if ( b )
                return true;
        }
    }

    return false;
}

uint Map::GetDistance( int ax, int ay, int bx, int by ) const
{
    return uint( sqrt( double( ( bx - ax ) * ( bx - ax ) + ( by - ay ) * ( by - ay ) ) ) + 0.5 );
}

double Map::GetLightValue( int x, int y ) const
{
    if ( x < 0 || x >= int( _width ) || y < 0 || y >= int( _height ) )
         throw std::runtime_error( "Calculating out of bounds light value" );

    double r = 0.0;
    ConstActorList t;
    GetActorsOnMap( t );
    foreach ( const Actor* a, t ) {
        int range = 0;
        if ( !a->Call< BILight >( &range ) )
            continue;

        if ( range > 0 ) {
            uint d = GetDistance( a->GetX(), a->GetY(), x, y );
            if ( int( d ) <= range ) {
                if ( IsInLOS( a->GetX(), a->GetY(), x, y ) )
                    r = std::max( r, 1.0 - double( d ) / double( range ) );
            }
        }
    }
    return r;
}

double Map::GetCalculatedLightValue( int x, int y ) const
{
    if ( x < 0 || x >= int( _width ) || y < 0 || y >= int( _height ) )
         throw std::runtime_error( "Calculating out of bounds light value" );

    return _lightValues[ x + y * _width ];
}

void Map::CalculateLightValues( int left, int top, int right, int bottom )
{
    for ( int x = std::max( 0, left ); x < right && x < int( _width ); x++ ) {
        for ( int y = std::max( 0, top ); y < bottom && y < int( _height ); y++ ) {
            _lightValues[ x + y * _width ] = 0.0;
        }
    }
    ConstActorList t;
    GetActorsOnMap( t );
    foreach ( const Actor* a, t ) {
        int range = 0;
        if ( !a->Call< BILight >( &range ) )
            continue;

        if ( range > 0 ) {
            for ( int x = std::max( 0, left ); x < right && x < int( _width ); x++ ) {
                for ( int y = std::max( 0, top ); y < bottom && y < int( _height ); y++ ) {
                    uint d = GetDistance( a->GetX(), a->GetY(), x, y );
                    if ( int( d ) <= range ) {
                        if ( IsInLOS( a->GetX(), a->GetY(), x, y ) )
                            _lightValues[ x + y * _width ] = std::max( _lightValues[ x + y * _width ], 1.0 - double( d ) / double( range ) );
                    }
                }
            }
        }
    }
}

void Map::Tick()
{
    typedef std::pair< ActorReference, int* > UpdatePair;
    std::vector< UpdatePair > t;

    foreach ( const MapActor &a, _actors )
        t.push_back( UpdatePair( ActorReference( a.a ), &a.t ) );

    foreach ( UpdatePair& p, t ) {
        ActorReference& a = p.first;
        int* t = p.second;

        if ( !a.IsValid() )
            continue;
        a->Call< BIOnTick >();
        if ( !a.IsValid() )
            continue;

        if ( *t > 0 )
            ( *t )--;
        else {
            int speedBase = 0;
            int speedMod  = 0;

            a->Call< BISpeed >   ( &speedBase );
            a->Call< BIOnAction >( &speedMod );
            *t = speedBase + speedMod;
        }
    }
}
